package org.hawksee.core.tools;

import java.util.Random;

import org.hawksee.core.data.BaseMonochromeImage;
import org.hawksee.core.data.Position;
import org.hawksee.core.data.Region;

/*
 * This class is responsible for registering one image to another.
 */
public class Registrator
{
  public enum Feature
  {
    GRADIENT,
    INTENSITY
  }
  private Feature feature = Feature.INTENSITY;
  private int pointsMatched = 32 * 32;
  private int allowedMove = 64;
  private int minimumOverlap = 20;
  private Random random;
  
  public Registrator()
  {
    this.random = new Random();
  }
  /*
   * Calculates the delta for a feature for a particular point.
   */
  private double calculateFeatureError(BaseMonochromeImage targetImage, 
      BaseMonochromeImage sourceImage, int x, int y)
  {
    switch(this.feature) {
      case GRADIENT:
        return this.calculateGradientError(targetImage, sourceImage, x, y);
      case INTENSITY:
        return this.calculateIntensityError(targetImage, sourceImage, x, y);
    }
    return -1.0;
  }
  /*
   * Calculates the delta for a gradient for a particular point.
   */
  private double calculateGradientError(BaseMonochromeImage targetImage, 
      BaseMonochromeImage sourceImage, int x, int y)
  {
    return (Math.pow(targetImage.getGradientX(x, y, true) -
        sourceImage.getGradientX(x, y, true), 2.0) + 
        Math.pow(targetImage.getGradientY(x, y, true) -
            sourceImage.getGradientY(x, y, true), 2.0));
  }
  /*
   * Calculates the delta for an intensity for a particular point.
   */
  private double calculateIntensityError(BaseMonochromeImage targetImage, 
      BaseMonochromeImage sourceImage, int x, int y)
  {
    return Math.pow(targetImage.getIntensityAbsolute(x, y) - 
        sourceImage.getIntensityAbsolute(x, y), 2.0);
  }
  /*
   * Scores the overlap, average difference/pixel
   */
  public double calculateOverlapError(BaseMonochromeImage targetImage, 
      BaseMonochromeImage sourceImage)
  {
    int pixelsTested = 0;
    double pixelsError = 0.0;
    double pixelError = 0.0;
    Region targetRegion = targetImage.getRegion();
    Region sourceRegion = sourceImage.getRegion();
    Region overlap = targetRegion.intersection(sourceRegion);
    
    // ensure there is actually an overlap
    if ((overlap.getWidth() <= 0) || (overlap.getHeight() <= 0)) {
      return Double.MAX_VALUE;
    }
    
    if (this.pointsMatched > 0) {
      int x;
      int y;
      for (int test=0; test < this.pointsMatched; test++) {
        x = overlap.getLeft() + random.nextInt(overlap.getWidth());
        y = overlap.getTop() + random.nextInt(overlap.getHeight());
        pixelError = this.calculateFeatureError(targetImage, sourceImage, x, y);
        if (pixelError >= 0.0) {
          pixelsError += pixelError;
          pixelsTested += 1;
        }
      }
    } else {
      // for each overlapped point, add the delta in
      for (int x=overlap.getLeft(); x < overlap.getRight(); x++) {
        for (int y=overlap.getTop(); y < overlap.getBottom(); y++) {
          pixelError = this.calculateFeatureError(targetImage, sourceImage, x, y);
          if (pixelError >= 0.0) {
            pixelsError += pixelError;
            pixelsTested += 1;
          }
        }
      }
    }
    
    // if no pixels were tested, return failed value
    if (pixelsTested <= 0) {
      return Double.MAX_VALUE;
    } else {
      return (pixelsError / (double)pixelsTested);
    }
  }
  /*
   * Scores the overlap, average difference/pixel
   */
  public double calculateOverlapError(BaseMonochromeImage targetImage, 
      BaseMonochromeImage sourceImage, int sourceX, int sourceY)
  {
    
    // determine if there is a temporary location
    Position originalPos = new Position(sourceImage.getX(), sourceImage.getY());
    sourceImage.moveTo(sourceX, sourceY);
      
    // find the overlap error
    double error = this.calculateOverlapError(targetImage, sourceImage);
    
    // move back to original position if the image was moved
    sourceImage.moveTo(originalPos.x, originalPos.y);
    return error;
  }
  /*
   * Calculates the x,y bounds for source image
   */
  public Region calculatePositionBounds(BaseMonochromeImage targetImage, 
      BaseMonochromeImage sourceImage)
  {
    if (this.allowedMove > 0) {
      return new Region(
          sourceImage.getX() - this.allowedMove,
          sourceImage.getY() - this.allowedMove,
          2 * this.allowedMove,
          2 * this.allowedMove);
    } else {
      // ensure that there will always be minimumOverlap x minimumOverlap to test
      return new Region(
          targetImage.getX() + this.minimumOverlap - sourceImage.getWidth(),
          targetImage.getY() + this.minimumOverlap - sourceImage.getHeight(),
          sourceImage.getWidth() + targetImage.getWidth() - 2 * this.minimumOverlap,
          sourceImage.getHeight() + targetImage.getHeight() - 2 * this.minimumOverlap);
    }
  }
  /*
   * Gets the feature set.
   */
  public Feature getFeature()
  {
    return this.feature;
  }
  /*
   * Moves an image into an optimal position, returns whether or not it moved
   */
  public boolean moveToOptimal(BaseMonochromeImage targetImage, 
      BaseMonochromeImage sourceImage)
  {
    // save off the original location
    int origX = sourceImage.getX();
    int origY = sourceImage.getY();
    
    // try to move to optimal
    this.moveToOptimal(targetImage, sourceImage, 
        this.calculatePositionBounds(targetImage, sourceImage), Double.MAX_VALUE);
    
    // return whether or not the image has moved
    return ((origX != sourceImage.getX()) || (origY != sourceImage.getY()));
  }
  /*
   * Moves an image to an optimal position with limits, returns if it moved
   */
  private void moveToOptimal(BaseMonochromeImage targetImage, 
      BaseMonochromeImage sourceImage, Region bounds, double maxRadius)
  {
    // use radial search
    int centerX = sourceImage.getX();
    int centerY = sourceImage.getY();
    int x,y;
    double error;
    boolean moved = false;
    double radiusErrorAdjustment;
    double radiusUsed = 0.0;
    double lowestError = this.calculateOverlapError(targetImage, sourceImage);
    double deltaTheta = Math.PI / 4.0;
    for (double radius=1.0; radius <= maxRadius; radius *= 1.5) {
      radiusErrorAdjustment = Math.sqrt(radius); 
      deltaTheta = Math.PI / (2 * Math.min(radius, 8.0));
      for (double theta=0.0; theta < 2 * Math.PI; theta += deltaTheta) {
        x = centerX + (int)(radius * Math.cos(theta));
        y = centerY + (int)(radius * Math.sin(theta));
        
        // skip this point if outside of search bounds
        if (!bounds.contains(x, y)) {
          continue;
        }
        
        // calculate the error
        error = (radiusErrorAdjustment * 
            this.calculateOverlapError(targetImage, sourceImage, x, y));
        if (error < lowestError) {
          radiusUsed = radius;
          lowestError = error;
          moved = true;
          sourceImage.moveTo(x, y);
        }
      }
    }
    
    // try again on new center
    if (moved) {
      this.moveToOptimal(targetImage, sourceImage, bounds, radiusUsed);
    }
  }
  /*
   * Sets the allowed move (absolute distance in a direction).
   * zero will be any match with enough overlap
   */
  public void setAllowedMove(int allowedMove)
  {
    this.allowedMove = allowedMove;
  }
  /*
   * Sets the feature set.
   */
  public void setFeature(Feature feature)
  {
    this.feature = feature;
  }
  /*
   * Sets the points to match in a pass.  zero will match all
   */
  public void setPointsMatched(int pointsMatched)
  {
    this.pointsMatched = pointsMatched;
  }
}
